{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "16bb4bc3",
   "metadata": {},
   "source": [
    "## Custom agent\n",
    "\n",
    "不管是任意的大模型，我们都可以根据大模型的特性来打造智能体。这通常是创建代理的最可靠方法。\n",
    "\n",
    "我们将首先在没有历史记录的情况下创建它，但随后我们将展示如何添加历史记录。需要历史记录才能启用对话。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0af14a1",
   "metadata": {},
   "source": [
    "## 加载LLM\n",
    "\n",
    "首先，让我们加载将用于控制代理的语言模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "016d09ea",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI, OpenAI\n",
    "\n",
    "openai_api_key = \"EMPTY\"\n",
    "openai_api_base = \"http://127.0.0.1:1234/v1\"\n",
    "model = ChatOpenAI(\n",
    "    openai_api_key=openai_api_key,\n",
    "    openai_api_base=openai_api_base,\n",
    "    temperature=0.3,\n",
    ")\n",
    "llm = model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "143aa5b4",
   "metadata": {},
   "source": [
    "## 定义工具\n",
    "\n",
    "接下来，让我们定义一些要使用的工具。让我们编写一个非常简单的 Python 函数来计算传入的单词的长度。\n",
    "\n",
    "请注意，这里我们使用的函数文档字符串非常重要。在此处阅读有关为什么会这样的更多信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "fa2269a9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain.agents import tool\n",
    "\n",
    "\n",
    "@tool\n",
    "def get_word_length(word: str) -> int:\n",
    "    \"\"\"Returns the length of a word.\"\"\"\n",
    "    return len(word)\n",
    "\n",
    "\n",
    "get_word_length.invoke(\"abc\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "18c75770",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'description': '多云', 'temperature': '23'}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import requests\n",
    "\n",
    "@tool\n",
    "def get_weather(location):\n",
    "    \"\"\"根据城市获取天气数据\"\"\"\n",
    "    api_key = \"SKcA5FGgmLvN7faJi\"\n",
    "    url = f\"https://api.seniverse.com/v3/weather/now.json?key={api_key}&location={location}&language=zh-Hans&unit=c\"\n",
    "    response = requests.get(url)\n",
    "    #print(location)\n",
    "    if response.status_code == 200:\n",
    "        data = response.json()\n",
    "        #print(data)\n",
    "        weather = {\n",
    "            'description':data['results'][0][\"now\"][\"text\"],\n",
    "            'temperature':data['results'][0][\"now\"][\"temperature\"]\n",
    "        }\n",
    "        return weather\n",
    "    else:\n",
    "        raise Exception(f\"失败接收天气信息：{response.status_code}\")\n",
    "            \n",
    "get_weather.invoke(\"广州\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 201,
   "id": "a78768f4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'title': '郭德纲_百度百科',\n",
       "  'content': 'October 31, 2016 - 郭德纲，1973年1月18日出生于中国天津市，祖籍中国山西省，中国内地相声演员、导演、编剧、歌手、演员、主持人、北京德云社创始人。1979年，开始学艺。1989年，到红桥文化馆工作。1998年，创办北京相声大会（德云社前...',\n",
       "  'url': 'https://baike.baidu.com/item/郭德纲/175780'},\n",
       " {'title': '郭德纲- 维基百科，自由的百科全书',\n",
       "  'content': 'February 9, 2024 - 郭德纲（1973年1月18日—），天津人，祖籍山西汾阳，中国相声演员，亦曾出演影视剧，以及多档电视节目主持人，北京德云社创始人之一。2004年冬天起受到媒体关注。曾长期自称“非著名相声演员”，其相声爱好者自称“...',\n",
       "  'url': 'https://zh.wikipedia.org/zh-hans/郭德纲'},\n",
       " {'title': '郭德纲- 维基百科，自由的百科全书',\n",
       "  'content': '郭德纲（1973年1月18日—），天津人，祖籍山西汾阳，中國相声演员，亦曾出演影视剧，以及多档电视节目主持人，北京德云社创始人之一。2004年冬天起受到媒体关注。',\n",
       "  'url': 'https://zh.wikipedia.org/wiki/%E9%83%AD%E5%BE%B7%E7%BA%B2'}]"
      ]
     },
     "execution_count": 201,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import requests\n",
    "import json\n",
    "# SearXNG 的实例 URL，你可以使用官方实例或自托管实例\n",
    "@tool\n",
    "def searxng_search(query):\n",
    "    \"\"\"输入搜索内容，使用 SearXNG 进行搜索。\"\"\"\n",
    "    SEARXNG_URL = 'http://127.0.0.1:6688'\n",
    "    SEARXNG_ENGINE_TOKEN = '123456789'\n",
    "    params = {}\n",
    "    # 设置搜索参数\n",
    "    params['q'] = query\n",
    "    params['format'] = 'json'  # 返回 JSON 格式的结果\n",
    "    params['token'] = SEARXNG_ENGINE_TOKEN  # 加入引擎令牌\n",
    "    # 发送 GET 请求\n",
    "    response = requests.get(SEARXNG_URL, params=params)\n",
    "    #return response.text\n",
    "    # 检查响应状态码\n",
    "    if response.status_code == 200:\n",
    "        res = response.json()\n",
    "        #print(res)\n",
    "        resList = []\n",
    "        for item in res['results']:\n",
    "            resList.append({\n",
    "                \"title\":item['title'],\n",
    "                \"content\":item['content'],\n",
    "                \"url\":item['url']\n",
    "            })\n",
    "            if len(resList) >= 3:\n",
    "                break\n",
    "        return resList \n",
    "    else:\n",
    "        response.raise_for_status()\n",
    "        \n",
    "searxng_search.invoke(\"郭德纲\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 144,
   "id": "b74be753",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"蔡徐坤（KUN），1998年8月2日出生于浙江省温州市，户籍湖南省吉首市，中国内地男歌手、演员、原创音乐制作人、MV导演。2012年8月，蔡徐坤参演的偶像剧《童话二分之一》播出，由此开始步入大众视线。2018年1月，参加竞演类综艺节目《偶像练习生》并以总票数第一正式出道，成为限定男团NINE PERCENT ...\\n\\n蔡徐坤（1998年8月2日 — ），浙江 温州人 ，中国大陆 歌手、原创音乐制作人及演员。 2012年通过参加综艺节目《向上吧!少年》进入中国内地娱乐圈 。 2015年参加中韩真人秀节目《星动亚洲》第一季和第二季，并于2016年10月成功通过10人男子组合swin正式出道。 在与依海文化提请解约后，他以个人 ...\\n\\nVideos · Play all · KUN - Spotlight (Documentary Film) · 蔡徐坤KUN 2023 WORLD TOUR「 KUALA LUMPUR」-《Spotlight》live · 蔡徐坤KUN 2023 WORLD TOUR 「 HONG KONG」.\\n\\nThere's an issue and the page could not be loaded. Reload page. 3M Followers, 37 Following, 243 Posts - See Instagram photos and videos from KUN (@caixukun)\\n\\nJuly 16, 2023 - We cannot provide a description for this page right now\""
      ]
     },
     "execution_count": 144,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_community.utilities import SearxSearchWrapper\n",
    "search = SearxSearchWrapper(\n",
    "    searx_host=\"http://127.0.0.1:6688\",\n",
    "    k = 5,\n",
    ")\n",
    "search.run(\"蔡徐坤\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 190,
   "id": "5a36f8df",
   "metadata": {},
   "outputs": [],
   "source": [
    "tools = [get_word_length,get_weather,searxng_search]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3f6ea25",
   "metadata": {},
   "source": [
    "## 创建提示\n",
    "\n",
    "现在让我们创建提示。由于 OpenAI 函数调用针对工具使用进行了微调，因此我们几乎不需要任何关于如何推理或如何输出格式的说明。我们将只有两个输入变量： input 和 agent_scratchpad 。 input 应为包含用户目标的字符串。 agent_scratchpad 应该是包含先前代理工具调用和相应工具输出的消息序列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 169,
   "id": "5b49ce6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "promptTemplate = \"\"\"尽可能的帮助用户回答任何问题。\n",
    "\n",
    "您可以使用以下工具来帮忙解决问题，如果已经知道了答案，也可以直接回答：\n",
    "\n",
    "{tools}\n",
    "\n",
    "回复格式说明\n",
    "----------------------------\n",
    "\n",
    "回复我时，请以以下两种格式之一输出回复：\n",
    "\n",
    "选项 1：如果您希望人类使用工具，请使用此选项。\n",
    "采用以下JSON模式格式化的回复内容：\n",
    "\n",
    "```json\n",
    "{{\n",
    "    \"reason\": string, \\\\ 叙述使用工具的原因\n",
    "    \"action\": string, \\\\ 要使用的工具。 必须是 {tool_names} 之一\n",
    "    \"action_input\": string \\\\ 工具的输入\n",
    "}}\n",
    "````\n",
    "\n",
    "选项2：如果您认为你已经有答案或者已经通过使用工具找到了答案，想直接对人类做出反应，请使用此选项。 采用以下JSON模式格式化的回复内容：\n",
    "\n",
    "```json\n",
    "{{\n",
    "  \"action\": \"Final Answer\",\n",
    "  \"answer\": string \\\\最终答复问题的答案放到这里！\n",
    "}}\n",
    "````\n",
    "\n",
    "用户的输入\n",
    "--------------------\n",
    "这是用户的输入（请记住通过单个选项，以JSON模式格式化的回复内容，不要回复其他内容）：\n",
    "\n",
    "{input}\n",
    "\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 170,
   "id": "d2274f63",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"你是非常强大的助手，你可以使用各种工具来完成人类交给的问题和任务。\",\n",
    "        ),\n",
    "        (\"user\", promptTemplate),\n",
    "        MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 171,
   "id": "f1c3bfc2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatPromptTemplate(input_variables=['agent_scratchpad', 'input'], input_types={'agent_scratchpad': typing.List[typing.Union[langchain_core.messages.ai.AIMessage, langchain_core.messages.human.HumanMessage, langchain_core.messages.chat.ChatMessage, langchain_core.messages.system.SystemMessage, langchain_core.messages.function.FunctionMessage, langchain_core.messages.tool.ToolMessage]]}, partial_variables={'tools': 'get_word_length: get_word_length(word: str) -> int - Returns the length of a word.\\nget_weather: get_weather(location) - 根据城市获取天气数据\\nsearxng_search: searxng_search(query) - 输入搜索内容，使用 SearXNG 进行搜索。', 'tool_names': 'get_word_length, get_weather, searxng_search'}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template='你是非常强大的助手，你可以使用各种工具来完成人类交给的问题和任何。')), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input', 'tool_names', 'tools'], template='尽可能的帮助用户回答任何问题。\\n\\n您可以使用以下工具来帮忙解决问题，如果已经知道了答案，也可以直接回答：\\n\\n{tools}\\n\\n回复格式说明\\n----------------------------\\n\\n回复我时，请以以下两种格式之一输出回复：\\n\\n选项 1：如果您希望人类使用工具，请使用此选项。\\n采用以下JSON模式格式化的回复内容：\\n\\n```json\\n{{\\n    \"reason\": string, \\\\ 叙述使用工具的原因\\n    \"action\": string, \\\\ 要使用的工具。 必须是 {tool_names} 之一\\n    \"action_input\": string \\\\ 工具的输入\\n}}\\n````\\n\\n选项2：如果您认为你已经有答案或者已经通过使用工具找到了答案，想直接对人类做出反应，请使用此选项。 采用以下JSON模式格式化的回复内容：\\n\\n```json\\n{{\\n  \"action\": \"Final Answer\",\\n  \"answer\": string \\\\最终答复问题的答案放到这里！\\n}}\\n````\\n\\n用户的输入\\n--------------------\\n这是用户的输入（请记住通过单个选项，以JSON模式格式化的回复内容，不要回复其他内容）：\\n\\n{input}\\n\\n')), MessagesPlaceholder(variable_name='agent_scratchpad')])"
      ]
     },
     "execution_count": 171,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain.tools.render import  render_text_description\n",
    "\n",
    "prompt = prompt.partial(\n",
    "        tools=render_text_description(list(tools)),\n",
    "        tool_names=\", \".join([t.name for t in tools]),\n",
    ")\n",
    "prompt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 198,
   "id": "31468772",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'get_word_length: get_word_length(word: str) -> int - Returns the length of a word.\\nget_weather: get_weather(location) - 根据城市获取天气数据\\nsearxng_search: searxng_search(query) - 输入搜索内容，使用 SearXNG 进行搜索。'"
      ]
     },
     "execution_count": 198,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "render_text_description(list(tools))\n",
    "\n",
    "# get_word_length: get_word_length(word: str) -> int - Returns the length of a word.\n",
    "# get_weather: get_weather(location) - 根据城市获取天气数据\n",
    "# searxng_search: searxng_search(query) - 输入搜索内容，使用 SearXNG 进行搜索。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ff6d290",
   "metadata": {},
   "source": [
    "## 将工具绑定到LLM\n",
    "\n",
    "在这种情况下，我们依赖于 OpenAI 工具调用 LLMs，它将工具作为一个单独的参数，并经过专门训练，知道何时调用这些工具。\n",
    "\n",
    "要将我们的工具传递给代理，我们只需要将它们格式化为 OpenAI 工具格式并将它们传递给我们的模型。（通过 bind 对函数进行 -ing，我们确保每次调用模型时都传入它们。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b46c172a",
   "metadata": {},
   "source": [
    "## 创建代理\n",
    "\n",
    "将这些部分放在一起，我们现在可以创建代理。我们将导入最后两个实用程序函数：一个用于格式化中间步骤（代理操作、工具输出对）以输入可发送到模型的消息的组件，以及一个用于将输出消息转换为代理操作/代理完成的组件。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 173,
   "id": "fe513870",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"TOOL RESPONSE: \\n---------------------\\n{observation}\\n\\nUSER'S INPUT\\n--------------------\\n\\nOkay, so what is the response to my last comment? If using information obtained from the tools you must mention it explicitly without mentioning the tool names - I have forgotten all TOOL RESPONSES! Remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else - even if you just want to respond to the user. Do NOT respond with anything except a JSON snippet no matter what!\""
      ]
     },
     "execution_count": 173,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain.agents.json_chat.prompt import TEMPLATE_TOOL_RESPONSE\n",
    "\n",
    "TEMPLATE_TOOL_RESPONSE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 174,
   "id": "0d37af86",
   "metadata": {},
   "outputs": [],
   "source": [
    "TEMPLATE_TOOL_RESPONSE = \"\"\"工具响应：\n",
    "---------------------\n",
    "{observation}\n",
    "\n",
    "用户的输入：\n",
    "---------------------\n",
    "请根据工具的响应判断，是否能够回答问题：\n",
    "\n",
    "{input}\n",
    "\n",
    "请根据工具响应的内容，思考接下来回复。回复格式严格按照前面所说的2种JSON回复格式，选择其中1种进行回复。请记住通过单个选项，以JSON模式格式化的回复内容，不要回复其他内容。\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 175,
   "id": "18aa7260",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
    "def format_log_to_messages(\n",
    "    query,\n",
    "    intermediate_steps,\n",
    "    template_tool_response,\n",
    "):\n",
    "    \"\"\"Construct the scratchpad that lets the agent continue its thought process.\"\"\"\n",
    "    thoughts: List[BaseMessage] = []\n",
    "    for action, observation in intermediate_steps:\n",
    "        thoughts.append(AIMessage(content=action.log))\n",
    "        human_message = HumanMessage(\n",
    "            content=template_tool_response.format(input=query,observation=observation)\n",
    "        )\n",
    "        thoughts.append(human_message)\n",
    "    return thoughts"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 176,
   "id": "94c5b783",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.agents.agent import AgentOutputParser\n",
    "from langchain_core.output_parsers.json import parse_json_markdown\n",
    "from langchain_core.exceptions import OutputParserException\n",
    "from langchain_core.agents import AgentAction, AgentFinish\n",
    "class JSONAgentOutputParser(AgentOutputParser):\n",
    "    \"\"\"Parses tool invocations and final answers in JSON format.\n",
    "\n",
    "    Expects output to be in one of two formats.\n",
    "\n",
    "    If the output signals that an action should be taken,\n",
    "    should be in the below format. This will result in an AgentAction\n",
    "    being returned.\n",
    "\n",
    "    ```\n",
    "    {\n",
    "      \"action\": \"search\",\n",
    "      \"action_input\": \"2+2\"\n",
    "    }\n",
    "    ```\n",
    "\n",
    "    If the output signals that a final answer should be given,\n",
    "    should be in the below format. This will result in an AgentFinish\n",
    "    being returned.\n",
    "\n",
    "    ```\n",
    "    {\n",
    "      \"action\": \"Final Answer\",\n",
    "      \"answer\": \"4\"\n",
    "    }\n",
    "    ```\n",
    "    \"\"\"\n",
    "\n",
    "    def parse(self, text):\n",
    "        try:\n",
    "            response = parse_json_markdown(text)\n",
    "            if isinstance(response, list):\n",
    "                # gpt turbo frequently ignores the directive to emit a single action\n",
    "                logger.warning(\"Got multiple action responses: %s\", response)\n",
    "                response = response[0]\n",
    "            if response[\"action\"] == \"Final Answer\":\n",
    "                return AgentFinish({\"output\": response[\"answer\"]}, text)\n",
    "            else:\n",
    "                return AgentAction(\n",
    "                    response[\"action\"], response.get(\"action_input\", {}), text\n",
    "                )\n",
    "        except Exception as e:\n",
    "            raise OutputParserException(f\"Could not parse LLM output: {text}\") from e\n",
    "\n",
    "    @property\n",
    "    def _type(self) -> str:\n",
    "        return \"json-agent\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 177,
   "id": "cc6392cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# from langchain.agents.output_parsers import JSONAgentOutputParser\n",
    "from langchain_core.runnables import Runnable, RunnablePassthrough\n",
    "\n",
    "\n",
    "agent = (\n",
    "        RunnablePassthrough.assign(\n",
    "            agent_scratchpad=lambda x: format_log_to_messages(\n",
    "                x[\"input\"],\n",
    "                x[\"intermediate_steps\"], \n",
    "                template_tool_response=TEMPLATE_TOOL_RESPONSE\n",
    "            )\n",
    "        )\n",
    "        | prompt\n",
    "        | llm\n",
    "        | JSONAgentOutputParser()\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 178,
   "id": "c178fe4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.agents import AgentExecutor\n",
    "\n",
    "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 179,
   "id": "02214206",
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"get_weather\",\n",
      "  \"action_input\": \"广州\"\n",
      "}\u001b[0m\u001b[33;1m\u001b[1;3m{'description': '阴', 'temperature': '19'}\u001b[0m\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"Final Answer\",\n",
      "  \"answer\": \"广州的天气是阴天，温度为19摄氏度。\"\n",
      "}\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': '广州的天气如何？', 'output': '广州的天气是阴天，温度为19摄氏度。'}"
      ]
     },
     "execution_count": 179,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "agent_executor.invoke({\"input\": \"广州的天气如何？\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 165,
   "id": "20990ab3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"searxng_search\",\n",
      "  \"action_input\": \"刘德华老婆\"\n",
      "}\u001b[0m\u001b[38;5;200m\u001b[1;3m[{'title': '刘德华54岁老婆朱丽倩近照,身体发福胖成大妈,女儿漂亮可爱像她_工作_鲜肉_成功', 'content': 'March 4, 2023 - 原标题：刘德华54岁老婆朱丽倩近照,身体发福胖成大妈,女儿漂亮可爱像她 · 人生如戏，我们上演着悲欢离合，试过欢声笑语，也试过痛哭流涕;人生如戏，我们经历过顺境逆境，尝过成功的甜，也尝过失败的苦。当中的滋...', 'url': 'https://www.sohu.com/a/649370669_121142690'}, {'title': '朱丽倩- 维基百科，自由的百科全书', 'content': 'November 7, 2023 - 朱丽倩（英语：Carol Chu，1966年4月6日—，本名朱丽卿），马来西亚华裔模特、艺人，祖籍福建漳州诏安，生于马来西亚槟城。舅舅是大马商人陈志远。2008年与香港知名艺人刘德华结婚。 · 朱丽倩为闽南人，生于马来西亚槟...', 'url': 'https://zh.wikipedia.org/zh-hans/朱麗倩'}, {'title': '朱丽蒨- 维基百科，自由的百科全书', 'content': '朱丽蒨（英语：Carol Chu，1966年4月6日—，旧误作朱丽倩），本名朱丽卿，马来西亚华裔模特、艺人，祖籍福建漳州诏安，生于马来西亚槟城。香港艺人刘德华妻子。 朱丽蒨.', 'url': 'https://zh.wikipedia.org/zh-hans/%E6%9C%B1%E9%BA%97%E8%92%A8'}]\u001b[0m------------------\n",
      "刘德华的老婆是谁？\n",
      "[{'title': '刘德华54岁老婆朱丽倩近照,身体发福胖成大妈,女儿漂亮可爱像她_工作_鲜肉_成功', 'content': 'March 4, 2023 - 原标题：刘德华54岁老婆朱丽倩近照,身体发福胖成大妈,女儿漂亮可爱像她 · 人生如戏，我们上演着悲欢离合，试过欢声笑语，也试过痛哭流涕;人生如戏，我们经历过顺境逆境，尝过成功的甜，也尝过失败的苦。当中的滋...', 'url': 'https://www.sohu.com/a/649370669_121142690'}, {'title': '朱丽倩- 维基百科，自由的百科全书', 'content': 'November 7, 2023 - 朱丽倩（英语：Carol Chu，1966年4月6日—，本名朱丽卿），马来西亚华裔模特、艺人，祖籍福建漳州诏安，生于马来西亚槟城。舅舅是大马商人陈志远。2008年与香港知名艺人刘德华结婚。 · 朱丽倩为闽南人，生于马来西亚槟...', 'url': 'https://zh.wikipedia.org/zh-hans/朱麗倩'}, {'title': '朱丽蒨- 维基百科，自由的百科全书', 'content': '朱丽蒨（英语：Carol Chu，1966年4月6日—，旧误作朱丽倩），本名朱丽卿，马来西亚华裔模特、艺人，祖籍福建漳州诏安，生于马来西亚槟城。香港艺人刘德华妻子。 朱丽蒨.', 'url': 'https://zh.wikipedia.org/zh-hans/%E6%9C%B1%E9%BA%97%E8%92%A8'}]\n",
      "\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"Final Answer\",\n",
      "  \"answer\": \"刘德华的老婆是朱丽倩（Carol Chu）。\"\n",
      "}\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': '刘德华的老婆是谁？', 'output': '刘德华的老婆是朱丽倩（Carol Chu）。'}"
      ]
     },
     "execution_count": 165,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "agent_executor.invoke({\"input\": \"刘德华的老婆是谁？\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68f909cf",
   "metadata": {},
   "source": [
    "## 添加历史记录\n",
    "\n",
    "这太棒了 - 我们有一个代理！但是，此代理是无状态的 - 它不记得有关先前交互的任何信息。这意味着你不能轻易地提出后续问题。让我们通过添加内存来解决这个问题。\n",
    "\n",
    "为此，我们需要做两件事：\n",
    "\n",
    "1. 在提示符中为内存变量添加一个位置\n",
    "2. 跟踪聊天记录\n",
    "\n",
    "首先，让我们在提示中添加一个内存位置。为此，我们通过为带有 键 \"chat_history\" 的消息添加一个占位符 .请注意，我们将其放在新用户输入的上方（以遵循对话流）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 192,
   "id": "f9745cf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import MessagesPlaceholder\n",
    "\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"你是非常强大的助手，你可以使用各种工具来完成人类交给的问题和任何。\",\n",
    "        ),\n",
    "        MessagesPlaceholder(variable_name=\"chat_history\"),\n",
    "        (\"user\", promptTemplate),\n",
    "        MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 193,
   "id": "11e38964",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import AIMessage, HumanMessage\n",
    "\n",
    "chat_history = []"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 196,
   "id": "3a565edc",
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt = prompt.partial(\n",
    "        tools=render_text_description(list(tools)),\n",
    "        tool_names=\", \".join([t.name for t in tools]),\n",
    ")\n",
    "\n",
    "agent = (\n",
    "        RunnablePassthrough.assign(\n",
    "            agent_scratchpad=lambda x: format_log_to_messages(\n",
    "                x[\"input\"],\n",
    "                x[\"intermediate_steps\"], \n",
    "                template_tool_response=TEMPLATE_TOOL_RESPONSE,\n",
    "            )\n",
    "        )\n",
    "        | prompt\n",
    "        | llm\n",
    "        | JSONAgentOutputParser()\n",
    "    )\n",
    "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 197,
   "id": "0b13f36d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"Final Answer\",\n",
      "  \"answer\": \"你的名字是老陈。\"\n",
      "}\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': '我叫什么名字？',\n",
       " 'chat_history': [HumanMessage(content='你好，我是老陈'),\n",
       "  AIMessage(content='你好，老陈。很高兴认识你！'),\n",
       "  HumanMessage(content='你好，我是老陈'),\n",
       "  AIMessage(content='你好，老陈。很高兴认识你！')],\n",
       " 'output': '你的名字是老陈。'}"
      ]
     },
     "execution_count": 197,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "\n",
    "\n",
    "chat_history.extend(\n",
    "    [\n",
    "        HumanMessage(content=\"你好，我是老陈\"),\n",
    "        AIMessage(content=\"你好，老陈。很高兴认识你！\"),\n",
    "    ]\n",
    ")\n",
    "agent_executor.invoke({\"input\": \"我叫什么名字？\", \"chat_history\": chat_history})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 199,
   "id": "10d9b189",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m{\n",
      "  \"action\": \"Final Answer\",\n",
      "  \"answer\": \"是的，朱丽倩曾出演过电影。她曾在1984年上映的马来西亚电影《神雕侠侣》中饰演小龙女一角。\"\n",
      "}\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': '刘德华老婆有演过电影吗',\n",
       " 'chat_history': [HumanMessage(content='你好，我是老陈'),\n",
       "  AIMessage(content='你好，老陈。很高兴认识你！'),\n",
       "  HumanMessage(content='你好，我是老陈'),\n",
       "  AIMessage(content='你好，老陈。很高兴认识你！'),\n",
       "  HumanMessage(content='刘德华的老婆是谁？'),\n",
       "  AIMessage(content='刘德华的老婆是朱丽倩（Carol Chu）。')],\n",
       " 'output': '是的，朱丽倩曾出演过电影。她曾在1984年上映的马来西亚电影《神雕侠侣》中饰演小龙女一角。'}"
      ]
     },
     "execution_count": 199,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chat_history.extend(\n",
    "    [\n",
    "        HumanMessage(content=\"刘德华的老婆是谁？\"),\n",
    "        AIMessage(content=\"刘德华的老婆是朱丽倩（Carol Chu）。\"),\n",
    "    ]\n",
    ")\n",
    "agent_executor.invoke({\"input\": \"刘德华老婆有演过电影吗\", \"chat_history\": chat_history})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d1a347a6",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
